# -*- coding: UTF-8 -*-
# Les vues génériques de l'interface d'administration, les modèles étant créés dynamiquement au moment de l'exécution.

from django.core import formfields, meta, template_loader
from django.core.exceptions import Http404, ObjectDoesNotExist, PermissionDenied
from django.core.extensions import DjangoContext as Context
from django.models.auth import log
from django.utils.html import strip_tags
from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
from django.utils.text import get_text_list
from django.conf.settings import ADMIN_MEDIA_PREFIX
import operator

# Le texte à afficher au sein des cellules des listes d'objets pour les valeurs vierges.
EMPTY_CHANGELIST_VALUE = '(Rien)'

def _get_mod_opts(app_label, module_name):
    "Une fonction assistante qui renvoie un tuple de la forme (module, options), et qui déclenche l'exception Http404 si nécessaire"
    try:
        mod = meta.get_module(app_label, module_name)
    except ImportError:
        raise Http404 # Le nom de l'application ou du module est invalide. Peut-être que l'application n'est pas incluse dans INSTALLED_APPS.
    opts = mod.Klass._meta
    if not opts.admin:
        raise Http404 # L'objet est valide mais aucune interface d'administration ne lui est associée.
    return mod, opts

def get_query_string(original_params, new_params={}, remove=[]):
    """
    >>> get_query_string({'prenom': 'adrian', 'nom': 'smith'})
    '?prenom=adrian&amp;nom=smith'
    >>> get_query_string({'prenom': 'adrian', 'nom': 'smith'}, {'prenom': 'john'})
    '?prenom=john&amp;nom=smith'
    >>> get_query_string({'test': 'oui'}, {'beurk': 'non'}, ['te'])
    '?beurk=non'
    """
    p = original_params.copy()
    for r in remove:
        for k in p.keys():
            if k.startswith(r):
                del p[k]
    for k, v in new_params.items():
        if p.has_key(k) and v is None:
            del p[k]
        elif v is not None:
            p[k] = v
    return '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20')

def index(request):
    t = template_loader.get_template('index')
    c = Context(request, {'title': 'Administration du site'})
    return HttpResponse(t.render(c))

def logout(request):
    request.session.delete()
    t = template_loader.get_template('logged_out')
    c = Context(request, {
        'title': "Vous êtes maintenant déconnecté",
    })
    return HttpResponse(t.render(c))

def change_list(request, app_label, module_name):
    from django.core import paginator
    from django.utils import dateformat
    from django.utils.dates import MONTHS
    from django.utils.html import escape
    import datetime

    # L'interface d'administration n'affichera un lien "Afficher tout" que si le nombre
    # total de résultats est inférieur ou égal à ce paramètre.
    MAX_SHOW_ALL_ALLOWED = 200

    DEFAULT_RESULTS_PER_PAGE = 100

    ALL_VAR = 'all'
    ORDER_VAR = 'o'
    ORDER_TYPE_VAR = 'ot'
    PAGE_VAR = 'p'
    SEARCH_VAR = 'q'
    IS_POPUP_VAR = 'pop'

    mod, opts = _get_mod_opts(app_label, module_name)
    if not request.user.has_perm(app_label + '.' + opts.get_change_permission()):
        raise PermissionDenied

    lookup_mod, lookup_opts = mod, opts

    if opts.one_to_one_field:
        lookup_mod = opts.one_to_one_field.rel.to.get_model_module()
        lookup_opts = lookup_mod.Klass._meta

    # On obtient les paramètres de recherche présents dans la "query string".
    try:
        page_num = int(request.GET.get(PAGE_VAR, 0))
    except ValueError:
        page_num = 0
    show_all = request.GET.has_key(ALL_VAR)
    is_popup = request.GET.has_key(IS_POPUP_VAR)
    params = dict(request.GET.copy())
    if params.has_key(PAGE_VAR):
        del params[PAGE_VAR]
    # Pour l'ordre de tri des objets, on regarde d'abord le paramètre "ordering" des options de
    # l'interface d'administration, puis on regarde l'ordre de tri par défaut de l'objet. Si ni
    # l'un ni l'autre de ces ordres de tri n'a été spécifié, on trie par défaut par identifiant
    # en ordre décroissant. Enfin, on regarde si un ordre de tri a été manuellement spécifié
    # dans la "query string".
    if lookup_opts.admin.ordering is not None:
        order_field, order_type = lookup_opts.admin.ordering
    elif lookup_opts.ordering:
        order_field, order_type = lookup_opts.ordering[0]
    else:
        order_field, order_type = lookup_opts.pk.name, 'DESC'
    if params.has_key(ORDER_VAR):
        try:
            order_key = int(params[ORDER_VAR])
            try:
                f = lookup_opts.get_field(lookup_opts.admin.list_display[order_key])
            except meta.FieldDoesNotExist:
                pass
            else:
                if not isinstance(f.rel, meta.ManyToOne) or not f.null:
                    order_field = f.name
        except (IndexError, ValueError):
            pass # Un ordre de tri invalide a été spécifié. On utilise dans ce cas l'ordre de tri par défaut.
    if params.has_key(ORDER_TYPE_VAR) and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
        order_type = params[ORDER_TYPE_VAR]
    query = request.GET.get(SEARCH_VAR, '')

    # On prépare les paramètres de recherche.
    lookup_params = params.copy()
    for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR):
        if lookup_params.has_key(i):
            del lookup_params[i]
    # Si le champ à partir duquel doit se faire le tri est impliqué dans une relation,
    # le tri doit se faire à partir de la valeur du champ dans la table liée par la
    # relation.
    lookup_order_field = order_field
    if isinstance(lookup_opts.get_field(order_field).rel, meta.ManyToOne):
        f = lookup_opts.get_field(order_field)
        lookup_order_field = '%s.%s' % (f.rel.to.db_table, f.rel.to.ordering[0][0])
    # On active l'option select_related si l'un des champs spécifiés par le
    # paramètre list_display est impliqué dans une relation.
    for field_name in lookup_opts.admin.list_display:
        try:
            f = lookup_opts.get_field(field_name)
        except meta.FieldDoesNotExist:
            pass
        else:
            if isinstance(f.rel, meta.ManyToOne):
                lookup_params['select_related'] = True
                break
    lookup_params['order_by'] = ((lookup_order_field, order_type),)
    if lookup_opts.admin.search_fields and query:
        or_queries = []
        for bit in query.split():
            or_query = []
            for field_name in lookup_opts.admin.search_fields:
                or_query.append(('%s__icontains' % field_name, bit))
            or_queries.append(or_query)
        lookup_params['_or'] = or_queries

    if opts.one_to_one_field:
        lookup_params.update(opts.one_to_one_field.rel.limit_choices_to)

    # On va chercher les résultats.
    try:
        p = paginator.ObjectPaginator(lookup_mod, lookup_params, DEFAULT_RESULTS_PER_PAGE)
    # On intercepte toutes les exceptions! Cela est nécessaire car on ne dispose d'aucun
    # autre moyen de valider les paramètres. Ces derniers peuvent en effet être invalides
    # pour différentes raisons : les arguments passés par mot-clé peuvent être incorrects,
    # les valeurs correspondant à ces arguments peuvent être d'un type incorrect (ce qui
    # a pour effet de déclencher une erreur au niveau de la base de données)...
    except:
        return HttpResponseRedirect(request.path)

    # On obtient le nombre total d'objets, aucun filtre n'étant encore appliqué.
    real_lookup_params = lookup_params.copy()
    del real_lookup_params['order_by']
    if real_lookup_params:
        full_result_count = lookup_mod.get_count()
    else:
        full_result_count = p.hits
    del real_lookup_params
    result_count = p.hits
    can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED
    multi_page = result_count > DEFAULT_RESULTS_PER_PAGE

    # On obtient la liste d'objets à afficher sur la page actuelle.
    if (show_all and can_show_all) or not multi_page:
        result_list = lookup_mod.get_list(**lookup_params)
    else:
        try:
            result_list = p.get_page(page_num)
        except paginator.InvalidPage:
            result_list = []

    # On s'occupe d'abord des filtres, car une classe CSS présente au début du document
    # nécessite de savoir assez tôt si les filtres sont disponibles.
    filter_template = []
    if lookup_opts.admin.list_filter and not opts.one_to_one_field:
        filter_fields = [lookup_opts.get_field(field_name) for field_name in lookup_opts.admin.list_filter]
        for f in filter_fields:
            # Le filtre s'applique à un champ impliqué dans une relation plusieurs-plusieurs ou plusieurs-un.
            if f.rel:
                if isinstance(f, meta.ManyToManyField):
                    lookup_kwarg = '%s__id__exact' % f.name
                    lookup_title = f.rel.to.verbose_name
                else:
                    lookup_kwarg = '%s__exact' % f.name
                    lookup_title = f.verbose_name
                lookup_val = request.GET.get(lookup_kwarg, None)
                lookup_choices = f.rel.to.get_model_module().get_list()
                if len(lookup_choices) > 1:
                    filter_template.append('<h3>Par "%s" :</h3>\n<ul>\n' % lookup_title)
                    filter_template.append('<li%s><a href="%s">Afficher tout</a></li>\n' % \
                        ((lookup_val is None and ' class="selected"' or ''),
                        get_query_string(params, {}, [lookup_kwarg])))
                    for val in lookup_choices:
                        filter_template.append('<li%s><a href="%s">%r</a></li>\n' % \
                            ((lookup_val == str(val.id) and ' class="selected"' or ''),
                            get_query_string(params, {lookup_kwarg: val.id}), val))
                    filter_template.append('</ul>\n\n')
            # Le filtre s'applique à un champ pour lequel il y a seulement un nombre fini de choix possibles.
            elif f.choices:
                lookup_kwarg = '%s__exact' % f.name
                lookup_val = request.GET.get(lookup_kwarg, None)
                filter_template.append('<h3>Par "%s" :</h3><ul>\n' % f.verbose_name)
                filter_template.append('<li%s><a href="%s">Afficher tout</a></li>\n' % \
                    ((lookup_val is None and ' class="selected"' or ''),
                    get_query_string(params, {}, [lookup_kwarg])))
                for k, v in f.choices:
                    filter_template.append('<li%s><a href="%s">%s</a></li>' % \
                        ((str(k) == lookup_val) and ' class="selected"' or '',
                        get_query_string(params, {lookup_kwarg: k}), v))
                filter_template.append('</ul>\n\n')
            # Le filtre s'applique à une date.
            elif isinstance(f, meta.DateField):
                today = datetime.date.today()
                one_week_ago = today - datetime.timedelta(days=7)
                field_generic = '%s__' % field_name
                filter_template.append('<h3>Par "%s" :</h3><ul>\n' % f.verbose_name)
                date_params = dict([(k, v) for k, v in params.items() if k.startswith(field_generic)])
                today_str = isinstance(f, meta.DateTimeField) and today.strftime('%d-%m-%Y 23:59:59') or today.strftime('%d-%m-%Y')
                for title, param_dict in (
                    ('Toute date', {}),
                    ('Aujourd\'hui', {'%s__year' % f.name: str(today.year), '%s__month' % f.name: str(today.month), '%s__day' % f.name: str(today.day)}),
                    ('Les 7 derniers jours', {'%s__gte' % f.name: one_week_ago.strftime('%d-%m-%Y'), '%s__lte' % f.name: today_str}),
                    ('Le mois actuel', {'%s__year' % f.name: str(today.year), '%s__month' % f.name: str(today.month)}),
                    ('L\'année actuelle', {'%s__year' % f.name: str(today.year)})
                ):
                    filter_template.append('<li%s><a href="%s">%s</a></li>\n' % \
                        ((date_params == param_dict) and ' class="selected"' or '',
                        get_query_string(params, param_dict, field_generic), title))
                filter_template.append('</ul>\n\n')
            elif isinstance(f, meta.BooleanField) or isinstance(f, meta.NullBooleanField):
                lookup_kwarg = '%s__exact' % f.name
                lookup_kwarg2 = '%s__isnull' % f.name
                lookup_val = request.GET.get(lookup_kwarg, None)
                lookup_val2 = request.GET.get(lookup_kwarg2, None)
                filter_template.append('<h3>Par "%s" :</h3><ul>\n' % f.verbose_name)
                for k, v in (('Afficher tout', None), ('Oui', 'True'), ('Non', 'False')):
                    filter_template.append('<li%s><a href="%s">%s</a></li>\n' % \
                        (((lookup_val == v and not lookup_val2) and ' class="selected"' or ''),
                        get_query_string(params, {lookup_kwarg: v}, [lookup_kwarg2]), k))
                if isinstance(f, meta.NullBooleanField):
                    filter_template.append('<li%s><a href="%s">%s</a></li>\n' % \
                        (((lookup_val2 == 'True') and ' class="selected"' or ''),
                        get_query_string(params, {lookup_kwarg2: 'True'}, [lookup_kwarg]), 'Inconnu'))
                filter_template.append('</ul>\n\n')
            else:
                pass # L'argument passé à "list_filter" est invalide

    raw_template = ['{% extends "base_site" %}\n']
    raw_template.append('{% block bodyclass %}change-list{% endblock %}\n')
    if not is_popup:
        raw_template.append('{%% block breadcrumbs %%}<div class="breadcrumbs"><a href="../../">Accueil</a> &rsaquo; Objets "%s"</div>{%% endblock %%}\n' % opts.verbose_name_plural)
    raw_template.append('{% block coltype %}flex{% endblock %}')
    raw_template.append('{% block content %}\n')
    raw_template.append('<div id="content-main">\n')
    if request.user.has_perm(app_label + '.' + lookup_opts.get_add_permission()):
        raw_template.append('<ul class="object-tools"><li><a href="add/%s" class="addlink">Ajouter un objet "%s"</a></li></ul>\n' % ((is_popup and '?_popup=1' or ''), opts.verbose_name))
    raw_template.append('<div class="module%s" id="changelist">\n' % (filter_template and ' filtered' or ''))

    # Le formulaire de recherche.
    if lookup_opts.admin.search_fields:
        raw_template.append('<div id="toolbar">\n<form id="changelist-search" action="" method="get">\n')
        raw_template.append('<label><img src="%simg/admin/icon_searchbox.png" /></label> ' % ADMIN_MEDIA_PREFIX)
        raw_template.append('<input type="text" size="40" name="%s" value="%s" id="searchbar" /> ' % \
            (SEARCH_VAR, escape(query)))
        raw_template.append('<input type="submit" value="OK" /> ')
        if result_count != full_result_count and not opts.one_to_one_field:
            raw_template.append('<span class="small quiet">%s résultat%s (<a href="?">sur %s</a>)</span>' % \
                (result_count, (result_count != 1 and 's' or ''), full_result_count))
        for k, v in params.items():
            if k != SEARCH_VAR:
                raw_template.append('<input type="hidden" name="%s" value="%s" />' % (escape(k), escape(v)))
        raw_template.append('</form></div>\n')
        raw_template.append('<script type="text/javascript">document.getElementById("searchbar").focus();</script>')

    # La navigation par date.
    if lookup_opts.admin.date_hierarchy:
        field_name = lookup_opts.admin.date_hierarchy

        year_field = '%s__year' % field_name
        month_field = '%s__month' % field_name
        day_field = '%s__day' % field_name
        field_generic = '%s__' % field_name
        year_lookup = params.get(year_field)
        month_lookup = params.get(month_field)
        day_lookup = params.get(day_field)

        raw_template.append('<div class="xfull">\n<ul class="toplinks">\n')
        if year_lookup and month_lookup and day_lookup:
            raw_template.append('<li class="date-back"><a href="%s">&lsaquo; %s %s </a></li>' % \
                (get_query_string(params, {year_field: year_lookup, month_field: month_lookup}, [field_generic]), MONTHS[int(month_lookup)], year_lookup))
            raw_template.append('<li>%s %s</li>' % (day_lookup, MONTHS[int(month_lookup)]))
        elif year_lookup and month_lookup:
            raw_template.append('<li class="date-back"><a href="%s">&lsaquo; %s</a></li>' % \
                (get_query_string(params, {year_field: year_lookup}, [field_generic]), year_lookup))
            date_lookup_params = lookup_params.copy()
            date_lookup_params.update({year_field: year_lookup, month_field: month_lookup})
            for day in getattr(lookup_mod, 'get_%s_list' % field_name)('day', **date_lookup_params):
                raw_template.append('<li><a href="%s">%s</a></li>' % \
                    (get_query_string(params, {year_field: year_lookup, month_field: month_lookup, day_field: day.day}, [field_generic]), day.strftime('%d %B')))
        elif year_lookup:
            raw_template.append('<li class="date-back"><a href="%s">&lsaquo; Toutes les dates</a></li>' % \
                get_query_string(params, {}, [year_field]))
            date_lookup_params = lookup_params.copy()
            date_lookup_params.update({year_field: year_lookup})
            for month in getattr(lookup_mod, 'get_%s_list' % field_name)('month', **date_lookup_params):
                raw_template.append('<li><a href="%s">%s %s</a></li>' % \
                    (get_query_string(params, {year_field: year_lookup, month_field: month.month}, [field_generic]), month.strftime('%B'), month.year))
        else:
            for year in getattr(lookup_mod, 'get_%s_list' % field_name)('year', **lookup_params):
                raw_template.append('<li><a href="%s">%s</a></li>\n' % \
                    (get_query_string(params, {year_field: year.year}, [field_generic]), year.year))
        raw_template.append('</ul><br class="clear" />\n</div>\n')

    # Les filtres.
    if filter_template:
        raw_template.append('<div id="changelist-filter">\n<h2>Filtrer</h2>\n')
        raw_template.extend(filter_template)
        raw_template.append('</div>')
    del filter_template

    # Le tableau des résultats.
    if result_list:
        # Les en-têtes du tableau.
        raw_template.append('<table cellspacing="0">\n<thead>\n<tr>\n')
        for i, field_name in enumerate(lookup_opts.admin.list_display):
            try:
                f = lookup_opts.get_field(field_name)
            except meta.FieldDoesNotExist:
                # Pour les valeurs de l'option list_display qui ne sont pas des
                # champs, on regarde si la fonction correspondante possède un
                # attribut "short_description". Si ce dernier n'existe pas, on
                # utilise le nom de la méthode. Oh, et __repr__ constitue un cas
                # particulier.
                if field_name == '__repr__':
                    header = lookup_opts.verbose_name
                else:
                    func = getattr(mod.Klass, field_name) # On laisse les exceptions AttributeError se propager.
                    try:
                        header = func.short_description
                    except AttributeError:
                        header = func.__name__
                # Les valeurs de l'option list_display qui ne sont pas des champs
                # ne peuvent pas constituer une clé de tri.
                raw_template.append('<th>%s</th>' % meta.capfirst(header))
            else:
                if isinstance(f.rel, meta.ManyToOne) and f.null:
                    raw_template.append('<th>%s</th>' % meta.capfirst(f.verbose_name))
                else:
                    th_classes = []
                    new_order_type = 'asc'
                    if field_name == order_field:
                        th_classes.append('sorted %sending' % order_type.lower())
                        new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type.lower()]
                    raw_template.append('<th%s><a href="%s">%s</a></th>' % \
                        ((th_classes and ' class="%s"' % ' '.join(th_classes) or ''),
                        get_query_string(params, {ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
                        meta.capfirst(f.verbose_name)))
        raw_template.append('</tr>\n</thead>\n')
        # Les lignes pour chaque résultat.
        pk = lookup_opts.pk.name
        for i, result in enumerate(result_list):
            raw_template.append('<tr class="row%s">\n' % (i % 2 + 1))
            for j, field_name in enumerate(lookup_opts.admin.list_display):
                row_class = ''
                try:
                    f = lookup_opts.get_field(field_name)
                except meta.FieldDoesNotExist:
                    # Pour les éléments de l'option list_display qui ne sont pas des champs,
                    # la valeur correspondante constitue le nom d'une méthode. On doit
                    # exécuter cette méthode.
                    try:
                        result_repr = strip_tags(str(getattr(result, field_name)()))
                    except ObjectDoesNotExist:
                        result_repr = EMPTY_CHANGELIST_VALUE
                else:
                    field_val = getattr(result, f.name)
                    # Les champs de type clé étrangère constituent des cas particuliers :
                    # on utilise pour ces champs la représentation de l'objet connexe.
                    if isinstance(f.rel, meta.ManyToOne):
                        if field_val is not None:
                            result_repr = getattr(result, 'get_%s' % f.rel.name)()
                        else:
                            result_repr = EMPTY_CHANGELIST_VALUE
                    # Les dates constituent des cas particuliers : elles doivent être
                    # formatées d'une certaine manière.
                    elif isinstance(f, meta.DateField):
                        if field_val:
                            if isinstance(f, meta.DateTimeField):
                                result_repr = dateformat.format(field_val, 'd M Y, H:i')
                            else:
                                result_repr = dateformat.format(field_val, 'd M Y')
                        else:
                            result_repr = EMPTY_CHANGELIST_VALUE
                        row_class = ' class="nowrap"'
                    # Les booléens constituent des cas particuliers : on utilise pour
                    # ces champs des images.
                    elif isinstance(f, meta.BooleanField) or isinstance(f, meta.NullBooleanField):
                        BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
                        result_repr = '<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val)
                    # Les champs ImageField constituent des cas particuliers : on utilise pour
                    # ces champs des miniatures.
                    elif isinstance(f, meta.ImageField):
                        from django.parts.media.photos import get_thumbnail_url
                        result_repr = '<img src="%s" alt="%s" title="%s" />' % (get_thumbnail_url(getattr(result, 'get_%s_url' % f.name)(), '120'), field_val, field_val)
                    # Les champs FloatField constituent des cas particuliers : on ajoute
                    # des zéros pour toujours obtenir le même nombre de décimales.
                    elif isinstance(f, meta.FloatField):
                        if field_val is not None:
                            result_repr = ('%%.%sf' % f.decimal_places) % field_val
                        else:
                            result_repr = EMPTY_CHANGELIST_VALUE
                    # Les champs pour lesquels il y a un nombre fini de choix possibles
                    # constituent des cas particuliers : on utilise pour ces champs la
                    # représentation du choix correspondant.
                    elif f.choices:
                        result_repr = dict(f.choices).get(field_val, EMPTY_CHANGELIST_VALUE)
                    else:
                        result_repr = strip_tags(str(field_val))
                # Certains navigateurs n'apprécient pas les balises "<td></td>" vides.
                if result_repr == '':
                    result_repr = '&nbsp;'
                if j == 0: # La première colonne constitue un cas particulier
                    result_id = getattr(result, pk)
                    raw_template.append('<th%s><a href="%s/"%s>%s</a></th>' % \
                        (row_class, result_id, (is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), result_repr))
                else:
                    raw_template.append('<td%s>%s</td>' % (row_class, result_repr))
            raw_template.append('</tr>\n')
        del result_list # on fait ça pour libérer un peu de mémoire
        raw_template.append('</table>\n')
    else:
        raw_template.append('<p>Aucun objet "%s" ne correspond à vos critères de recherche.</p>' % opts.verbose_name)

    # Pagination.
    raw_template.append('<p class="paginator">')
    if (show_all and can_show_all) or not multi_page:
        pass
    else:
        raw_template.append('Page &rsaquo; ')
        ON_EACH_SIDE = 3
        ON_ENDS = 2
        DOT = '.'
        # S'il y a 10 pages ou moins, on affiche des liens menant à chaque page.
        # Sinon, on joue les beaux gosses
        if p.pages <= 10:
            page_range = range(p.pages)
        else:
            # Pour la pagination, on insère des liens "intelligents" : il y aura
            # toujours ON_ENDS liens aux 2 extrémités de la liste de pages et il y
            # aura toujours ON_EACH_SIDE liens à gauche et à droite du lien menant
            # à la page actuelle.
            page_range = []
            if page_num > (ON_EACH_SIDE + ON_ENDS):
                page_range.extend(range(0, ON_EACH_SIDE - 1))
                page_range.append(DOT)
                page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1))
            else:
                page_range.extend(range(0, page_num + 1))
            if page_num < (p.pages - ON_EACH_SIDE - ON_ENDS - 1):
                page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1))
                page_range.append(DOT)
                page_range.extend(range(p.pages - ON_ENDS, p.pages))
            else:
                page_range.extend(range(page_num + 1, p.pages))
        for i in page_range:
            if i == DOT:
                raw_template.append('... ')
            elif i == page_num:
                raw_template.append('<span class="this-page">%d</span> ' % (i+1))
            else:
                raw_template.append('<a href="%s"%s>%d</a> ' % \
                    (get_query_string(params, {PAGE_VAR: i}), (i == p.pages-1 and ' class="end"' or ''), i+1))
    raw_template.append('%s objet%s "%s"' % (result_count, result_count != 1 and 's' or '', result_count == 1 and opts.verbose_name or opts.verbose_name_plural))
    if can_show_all and not show_all and multi_page:
        raw_template.append('&nbsp;&nbsp;<a href="%s" class="showall">Afficher tout</a>' % \
            get_query_string(params, {ALL_VAR: ''}))
    raw_template.append('</p>')

    raw_template.append('</div>\n</div>')
    raw_template.append('{% endblock %}\n')
    t = template_loader.get_template_from_string(''.join(raw_template))
    c = Context(request, {
        'title': (is_popup and 'Sélection d\'un objet "%s"' % opts.verbose_name or 'Sélection d\'un objet "%s" à modifier' % opts.verbose_name),
        'is_popup': is_popup,
    })
    return HttpResponse(t.render(c))

def _get_flattened_data(field, val):
    """
    Renvoie un dictionnaire dont chaque paire associe le nom d'un champ du
    manipulateur correspondant au champ passé en argument, avec sa valeur,
    "aplatie" pour les besoins de l'interface d'administration. L'argument
    val est une instance de la valeur du champ spécifié.
    """
    if isinstance(field, meta.DateTimeField):
        date_field, time_field = field.get_manipulator_field_names('')
        return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''),
                time_field: (val is not None and val.strftime("%H:%M:%S") or '')}
    elif isinstance(field, meta.DateField):
        return {field.name: (val is not None and val.strftime("%Y-%m-%d") or '')}
    elif isinstance(field, meta.TimeField):
        return {field.name: (val is not None and val.strftime("%H:%M:%S") or '')}
    else:
        return {field.name: val}

use_raw_id_admin = lambda field: isinstance(field.rel, meta.ManyToOne) and field.rel.raw_id_admin

def _get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects):
    t = ['<div class="submit-row">']
    if change or show_delete:
        t.append('{%% if perms.%s.%s %%}{%% if not is_popup %%}<p class="float-left"><a href="delete/" class="deletelink">Supprimer</a></p>{%% endif %%}{%% endif %%}' % \
            (app_label, opts.get_delete_permission()))
    if change and opts.admin.save_as:
        t.append('{%% if not is_popup %%}<input type="submit" value="Enregistrer en tant que nouvel objet" name="_saveasnew" %s/>{%% endif %%}' % \
            (ordered_objects and change and 'onclick="submitOrderForm();"' or ''))
    if not opts.admin.save_as or add:
        t.append('{%% if not is_popup %%}<input type="submit" value="Enregistrer puis ajouter un nouvel objet" name="_addanother" %s/>{%% endif %%}' % \
            (ordered_objects and change and 'onclick="submitOrderForm();"' or ''))
    t.append('<input type="submit" value="Enregistrer sans quitter l\'écran de modification" name="_continue" %s/>' % \
        (ordered_objects and change and 'onclick="submitOrderForm();"' or ''))
    t.append('<input type="submit" value="Enregistrer" class="default" %s/>' % \
        (ordered_objects and change and 'onclick="submitOrderForm();"' or ''))
    t.append('</div>\n')
    return t

def _get_template(opts, app_label, add=False, change=False, show_delete=False, form_url=''):
    ordered_objects = opts.get_ordered_objects()[:]
    auto_populated_fields = [f for f in opts.fields if f.prepopulate_from]
    t = ['{% extends "base_site" %}\n']
    t.append('{% block extrahead %}')

    # On ajoute toutes les importations JavaScript qui sont nécessaires.
    javascript_imports = ['%sjs/core.js' % ADMIN_MEDIA_PREFIX, '%sjs/admin/RelatedObjectLookups.js' % ADMIN_MEDIA_PREFIX]
    if 'collapse' in ' '.join([f[1].get('classes', '') for f in opts.admin.fields]):
        javascript_imports.append('%sjs/admin/CollapsedFieldsets.js' % ADMIN_MEDIA_PREFIX)
    if auto_populated_fields:
        javascript_imports.append('%sjs/urlify.js' % ADMIN_MEDIA_PREFIX)
    if opts.has_field_type(meta.DateTimeField) or opts.has_field_type(meta.TimeField) or opts.has_field_type(meta.DateField):
        javascript_imports.extend(['%sjs/calendar.js' % ADMIN_MEDIA_PREFIX, '%sjs/admin/DateTimeShortcuts.js' % ADMIN_MEDIA_PREFIX])
    if ordered_objects:
        javascript_imports.extend(['%sjs/getElementsBySelector.js' % ADMIN_MEDIA_PREFIX, '%sjs/dom-drag.js' % ADMIN_MEDIA_PREFIX, '%sjs/admin/ordering.js' % ADMIN_MEDIA_PREFIX])
    if opts.admin.js:
        javascript_imports.extend(opts.admin.js)
    for _, options in opts.admin.get_field_objs(opts):
        try:
            for field_list in options['fields']:
                for f in field_list:
                    if f.rel and isinstance(f, meta.ManyToManyField) and f.rel.filter_interface:
                        javascript_imports.extend(['%sjs/SelectBox.js' % ADMIN_MEDIA_PREFIX, '%sjs/SelectFilter2.js' % ADMIN_MEDIA_PREFIX])
                        raise StopIteration
        except StopIteration:
            break
    for j in javascript_imports:
        t.append('<script type="text/javascript" src="%s"></script>' % j)

    t.append('{% endblock %}\n')
    if ordered_objects:
        coltype = 'colMS'
    else:
        coltype = 'colM'
    t.append('{%% block coltype %%}%s{%% endblock %%}\n' % coltype)
    t.append('{%% block bodyclass %%}%s-%s change-form{%% endblock %%}\n' % (app_label, opts.object_name.lower()))
    breadcrumb_title = add and "Ajout d'un objet \"%s\"" % opts.verbose_name or '{{ original|striptags|truncatewords:"18" }}'
    t.append('{%% block breadcrumbs %%}{%% if not is_popup %%}<div class="breadcrumbs"><a href="../../../">Accueil</a> &rsaquo; <a href="../">Objets "%s"</a> &rsaquo; %s</div>{%% endif %%}{%% endblock %%}\n' % \
        (opts.verbose_name_plural, breadcrumb_title))
    t.append('{% block content %}<div id="content-main">\n')
    if change:
        t.append('{% if not is_popup %}')
        t.append('<ul class="object-tools"><li><a href="history/" class="historylink">Historique</a></li>')
        if hasattr(opts.get_model_module().Klass, 'get_absolute_url'):
            t.append('<li><a href="/r/%s/{{ object_id }}/" class="viewsitelink">Voir l\'objet sur le site</a></li>' % opts.get_content_type_id())
        t.append('</ul>\n')
        t.append('{% endif %}')
    t.append('<form ')
    if opts.has_field_type(meta.FileField):
        t.append('enctype="multipart/form-data" ')
    t.append('action="%s" method="post">\n' % form_url)
    t.append('{% if is_popup %}<input type="hidden" name="_popup" value="1">{% endif %}')
    if opts.admin.save_on_top:
        t.extend(_get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects))
    t.append('{% if form.error_dict %}<p class="errornote">Veuillez corriger la ou les erreurs suivantes :</p>{% endif %}\n')
    for fieldset_name, options in opts.admin.get_field_objs(opts):
        t.append('<fieldset class="module aligned %s">\n\n' % options.get('classes', ''))
        if fieldset_name:
            t.append('<h2>%s</h2>\n' % fieldset_name)
        for field_list in options['fields']:
            t.append(_get_admin_field(field_list, 'form.', False, add, change))
            for f in field_list:
                if f.rel and isinstance(f, meta.ManyToManyField) and f.rel.filter_interface:
                    t.append('<script type="text/javascript">addEvent(window, "load", function(e) { SelectFilter.init("id_%s", "%s", %s); });</script>\n' % (f.name, f.verbose_name, f.rel.filter_interface-1))
        t.append('</fieldset>\n')
    if ordered_objects and change:
        t.append('<fieldset class="module"><h2>Ordre de tri de l\'objet</h2>')
        t.append('<div class="form-row{% if form.order_.errors %} error{% endif %} ">\n')
        t.append('{% if form.order_.errors %}{{ form.order_.html_error_list }}{% endif %}')
        t.append('<p><label for="id_order_">Ordre :</label> {{ form.order_ }}</p>\n')
        t.append('</div></fieldset>\n')
    for rel_obj, rel_field in opts.get_inline_related_objects():
        var_name = rel_obj.object_name.lower()
        field_list = [f for f in rel_obj.fields + rel_obj.many_to_many if f.editable and f != rel_field]
        t.append('<fieldset class="module%s">\n' % ((rel_field.rel.edit_inline_type != meta.TABULAR) and ' aligned' or ''))
        view_on_site = ''
        if change and hasattr(rel_obj, 'get_absolute_url'):
            view_on_site = '{%% if %s.original %%}<a href="/r/{{ %s.content_type_id }}/{{ %s.original.id }}/">Voir l\'objet sur le site</a>{%% endif %%}' % (var_name, var_name, var_name)
        if rel_field.rel.edit_inline_type == meta.TABULAR:
            t.append('<h2>Objets "%s"</h2>\n<table>\n' % rel_obj.verbose_name_plural)
            t.append('<thead><tr>')
            for f in field_list:
                if isinstance(f, meta.AutoField):
                    continue
                t.append('<th%s>%s</th>' % (f.blank and ' class="optional"' or '', meta.capfirst(f.verbose_name)))
            t.append('</tr></thead>\n')
            t.append('{%% for %s in form.%s %%}\n' % (var_name, rel_obj.module_name))
            if change:
                for f in field_list:
                    if use_raw_id_admin(f):
                        t.append('{%% if %s.original %%}' % var_name)
                        t.append('<tr class="row-label {% cycle row1,row2 %}">')
                        t.append('<td colspan="%s"><strong>{{ %s.original }}</strong></td>' % (30, var_name))
                        t.append('</tr>{% endif %}\n')
                        break
            t.append('{%% if %s %%}\n' % ' or '.join(['%s.%s.errors' % (var_name, f.name) for f in field_list]))
            t.append('<tr class="errorlist"><td colspan="%s">%s</td></tr>\n{%% endif %%}\n' % \
                (len(field_list), ''.join(['{{ %s.%s.html_error_list }}' % (var_name, f.name) for f in field_list])))
            t.append('<tr class="{% cycle row1,row2 %}">\n')
            hidden_fields = []
            for f in field_list:
                form_widget = _get_admin_field_form_widget(f, var_name+'.', True, add, change)
                # On ne place pas les champs AutoField au sein d'une balise <td>, car ils sont cachés.
                if not isinstance(f, meta.AutoField):
                    # Les champs pour lesquels raw_id_admin=True se voient affecter la classe CSS "nowrap".
                    if use_raw_id_admin(f):
                        t.append('<td class="nowrap {%% if %s.%s.errors %%}error"{%% endif %%}">%s</td>\n' % (var_name, f.name, form_widget))
                    else:
                        t.append('<td{%% if %s.%s.errors %%} class="error"{%% endif %%}>%s</td>\n' % (var_name, f.name, form_widget))
                else:
                    hidden_fields.append(form_widget)
            if hasattr(rel_obj, 'get_absolute_url'):
                t.append('<td>%s</td>\n' % view_on_site)
            t.append('</tr>\n')
            t.append('{% endfor %}\n</table>\n')
            # On écrit les champs cachés. Si on ne les a pas écrits plus tôt,
            # c'est parce qu'on aurait alors écrit du code HTML invalide.
            t.append('{%% for %s in form.%s %%}\n' % (var_name, rel_obj.module_name))
            t.extend(hidden_fields)
            t.append('{% endfor %}\n')
        else: # Ici, edit_inline_type == STACKED
            t.append('{%% for %s in form.%s %%}' % (var_name, rel_obj.module_name))
            t.append('<h2>Objet "%s" n°{{ forloop.counter }}</h2>' % rel_obj.verbose_name)
            if view_on_site:
                t.append('<p>%s</p>' % view_on_site)
            for f in field_list:
                # On ne place pas les champs AutoField au sein d'un widget : on utilise simplement le champ correspondant.
                if isinstance(f, meta.AutoField):
                    t.append(_get_admin_field_form_widget(f, var_name+'.', True, add, change))
                else:
                    t.append(_get_admin_field([f], var_name+'.', True, add, change))
            t.append('{% endfor %}\n')
        t.append('</fieldset>\n')
    t.extend(_get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects))
    if add:
        # On donne le focus au premier champ du formulaire, à condition que ce formulaire soit un formulaire d'ajout.
        t.append('<script type="text/javascript">document.getElementById("id_%s").focus();</script>' % \
            opts.admin.get_field_objs(opts)[0][1]['fields'][0][0].get_manipulator_field_names('')[0])
    if auto_populated_fields:
        t.append('<script type="text/javascript">')
        for field in auto_populated_fields:
            if change:
                t.append('document.getElementById("id_%s")._changed = true;' % field.name)
            else:
                t.append('document.getElementById("id_%s").onchange = function() { this._changed = true; };' % field.name)
            for f in field.prepopulate_from:
                t.append('document.getElementById("id_%s").onkeyup = function() { var e = document.getElementById("id_%s"); if (!e._changed) { e.value = URLify(%s, %s);}};' % \
                    (f, field.name, ' + " " + '.join(['document.getElementById("id_%s").value' % g for g in field.prepopulate_from]), field.maxlength))
        t.append('</script>\n')
    if change and ordered_objects:
        t.append('{% if form.order_objects %}<ul id="orderthese">{% for object in form.order_objects %}')
        t.append('<li id="p{%% firstof %(x)s %%}"><span id="handlep{%% firstof %(x)s %%}">{{ object|truncatewords:"5" }}</span></li>' % \
            {'x': ' '.join(['object.%s' % o.pk.name for o in ordered_objects])})
        t.append('{% endfor %}</ul>{% endif %}\n')
    t.append('</form>\n</div>\n{% endblock %}')
    return ''.join(t)

def _get_admin_field(field_list, name_prefix, rel, add, change):
    "Renvoie le code HTML permettant de modifier la valeur des champs de la liste de champs spécifiée; ce code HTML sera inclus dans un modèle de l'interface d'administration."
    field_names = []
    for f in field_list:
        field_names.extend(f.get_manipulator_field_names(name_prefix))
    div_class_names = ['form-row', '{%% if %s %%} error{%% endif %%}' % ' or '.join(['%s.errors' % n for n in field_names])]
    # On suppose que les champs BooleanField ne s'empileront pas les uns à la suite des autres!
    if isinstance(field_list[0], meta.BooleanField):
        div_class_names.append('checkbox-row')
    t = []
    t.append('<div class="%s">\n' % ' '.join(div_class_names))
    for n in field_names:
        t.append('{%% if %s.errors %%}{{ %s.html_error_list }}{%% endif %%}\n' % (n, n))
    for i, field in enumerate(field_list):
        label_name = 'id_%s%s' % ((rel and "%s{{ forloop.counter0 }}." % name_prefix or ""), field.get_manipulator_field_names('')[0])
        # Les champs BooleanField constituent un cas particulier, car la case à cocher qui leur
        # correspond doit apparaître à *gauche* de l'étiquette du champ.
        if isinstance(field, meta.BooleanField):
            t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change))
            t.append(' <label for="%s" class="vCheckboxLabel">%s</label>' % (label_name, meta.capfirst(field.verbose_name)))
        else:
            class_names = []
            if not field.blank:
                class_names.append('required')
            if i > 0:
                class_names.append('inline')
            t.append('<label for="%s"%s>%s&nbsp;:</label> ' % (label_name, class_names and ' class="%s"' % ' '.join(class_names) or '', meta.capfirst(field.verbose_name)))
            t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change))
        if change and use_raw_id_admin(field):
            obj_repr = '%soriginal.get_%s|truncatewords:"14"' % (rel and name_prefix or '', field.rel.name)
            t.append('{%% if %s %%}&nbsp;<strong>{{ %s }}</strong>{%% endif %%}' % (obj_repr, obj_repr))
        if field.help_text:
            t.append('<p class="help">%s</p>\n' % field.help_text)
    t.append('</div>\n\n')
    return ''.join(t)

def _get_admin_field_form_widget(field, name_prefix, rel, add, change):
    "Renvoie JUSTE le widget du champ de formulaire correspondant au champ spécifié, pour les besoins de l'interface d'administration."
    field_names = field.get_manipulator_field_names(name_prefix)
    if isinstance(field, meta.DateTimeField):
        return '<p class="datetime">Date : {{ %s }}<br />Heure : {{ %s }}</p>' % tuple(field_names)
    t = ['{{ %s }}' % n for n in field_names]
    if change and isinstance(field, meta.FileField):
        return '{%% if %soriginal.%s %%}Actuellement : <a href="{{ %soriginal.get_%s_url }}">{{ %soriginal.%s }}</a><br />Modifier : %s{%% else %%}%s{%% endif %%}' % \
            (name_prefix, field.name, name_prefix, field.name, name_prefix, field.name, ''.join(t), ''.join(t))
    field_id = 'id_%s%s' % ((rel and "%s{{ forloop.counter0 }}." % name_prefix or ""), field.get_manipulator_field_names('')[0])
    # Les champs pour lesquels raw_id_admin=True se voient ajouter près d'eux un petit lien permettant de rechercher un objet
    if use_raw_id_admin(field):
        t.append(' <a href="../../../%s/%s/" class="related-lookup" id="lookup_%s" onclick="return showRelatedObjectLookupPopup(this);">' % \
                    (field.rel.to.app_label, field.rel.to.module_name, field_id))
        t.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="Rechercher un objet" /></a>' % ADMIN_MEDIA_PREFIX)
    # les champs impliqués dans une relation avec un objet possédant sa propre interface
    # d'administration se voient ajouter près d'eux un lien "ajouter un autre objet", à
    # condition que l'option raw_admin ne soit pas active pour ces champs (car dans ce
    # cas ils se voient déjà ajouter un bouton "ajouter" dans une fenêtre popup)
    elif field.rel and isinstance(field.rel, meta.ManyToOne) and field.rel.to.admin:
        t.append('{%% if perms.%s.%s %%}' % (field.rel.to.app_label, field.rel.to.get_add_permission()))
        t.append(' <a href="../../../%s/%s/add/" class="add-another" id="add_%s" onclick="return showAddAnotherPopup(this);">' % \
                    (field.rel.to.app_label, field.rel.to.module_name, field_id))
        t.append('<img src="%simg/admin/icon_addlink.gif" width="10" height="10" alt="Ajouter un autre objet de ce type" /></a>' % ADMIN_MEDIA_PREFIX)
        t.append('{% endif %}')
    return ''.join(t)

def add_stage(request, app_label, module_name, show_delete=False, form_url='', post_url='../', post_url_continue='../%s/', object_id_override=None):
    mod, opts = _get_mod_opts(app_label, module_name)
    if not request.user.has_perm(app_label + '.' + opts.get_add_permission()):
        raise PermissionDenied
    manipulator = mod.AddManipulator()
    if request.POST:
        new_data = request.POST.copy()
        if opts.has_field_type(meta.FileField):
            new_data.update(request.FILES)
        errors = manipulator.get_validation_errors(new_data)
        if not errors and not request.POST.has_key("_preview"):
            manipulator.do_html2python(new_data)
            new_object = manipulator.save(new_data)
            log.log_action(request.user.id, opts.get_content_type_id(), getattr(new_object, opts.pk.name), repr(new_object), log.ADDITION)
            msg = 'L\'objet "%s / %s" a bien été ajouté.' % (opts.verbose_name, new_object)
            # Ici, on distingue plusieurs cas correspondant aux différents modes d'enregistrement
            # possibles en contrôlant la présence de clés particulières au sein de la variable
            # request.POST.
            if request.POST.has_key("_continue"):
                request.user.add_message("%s Vous pouvez le modifier ci-dessous." % msg)
                if request.POST.has_key("_popup"):
                    post_url_continue += "?_popup=1"
                return HttpResponseRedirect(post_url_continue % new_object.id)
            if request.POST.has_key("_popup"):
                return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, %s, "%s");</script>' % \
                    (getattr(new_object, opts.pk.name), repr(new_object).replace('"', '\\"')))
            elif request.POST.has_key("_addanother"):
                request.user.add_message("%s Vous pouvez ajouter un autre objet \"%s\" ci-dessous." % (msg, opts.verbose_name))
                return HttpResponseRedirect(request.path)
            else:
                request.user.add_message(msg)
                return HttpResponseRedirect(post_url)
        if request.POST.has_key("_preview"):
            manipulator.do_html2python(new_data)
    else:
        new_data = {}
        # On ajoute les données par défaut.
        for f in opts.fields:
            if f.has_default():
                new_data.update(_get_flattened_data(f, f.get_default()))
            # Pour les champs impliqués dans une relation plusieurs-un qui sont obligatoires et
            # pour lesquels il n'y a qu'un seul choix disponible, on sélectionne cet unique choix.
            # Note : on doit regarder si la longueur de l'attribut choices est *2* et non pas 1
            # car les champs SelectField possèdent toujours une valeur initiale "vierge".
            elif not f.blank and ((isinstance(f.rel, meta.ManyToOne) and not f.rel.raw_id_admin) or f.choices) and len(manipulator[f.name].choices) == 2:
                new_data[f.name] = manipulator[f.name].choices[1][0]
        # Pour les champs impliqués dans une relation plusieurs-plusieurs qui sont obligatoires et
        # pour lesquels il n'y a qu'un seul choix disponible, on sélectionne cet unique choix.
        for f in opts.many_to_many:
            if not f.blank and not f.rel.edit_inline and len(manipulator[f.name].choices) == 1:
                new_data[f.name] = [manipulator[f.name].choices[0][0]]
        # On ajoute les données par défaut pour les objets connexes.
        for rel_opts, rel_field in opts.get_inline_related_objects():
            var_name = rel_opts.object_name.lower()
            for i in range(rel_field.rel.num_in_admin):
                for f in rel_opts.fields + rel_opts.many_to_many:
                    if f.has_default():
                        for field_name in f.get_manipulator_field_names(''):
                            new_data['%s.%d.%s' % (var_name, i, field_name)] = f.get_default()
        # Les valeurs définies dans request.GET écrasent, lorsqu'elles existent, les valeurs par défaut.
        new_data.update(request.GET)
        errors = {}

    # On remplit l'objet FormWrapper.
    form = formfields.FormWrapper(manipulator, new_data, errors)
    for rel_opts, rel_field in opts.get_inline_related_objects():
        var_name = rel_opts.object_name.lower()
        wrapper = []
        for i in range(rel_field.rel.num_in_admin):
            collection = {}
            for f in rel_opts.fields + rel_opts.many_to_many:
                if f.editable and f != rel_field and not isinstance(f, meta.AutoField):
                    for field_name in f.get_manipulator_field_names(''):
                        full_field_name = '%s.%d.%s' % (var_name, i, field_name)
                        collection[field_name] = formfields.FormFieldWrapper(manipulator[full_field_name], new_data.get(full_field_name, ''), errors.get(full_field_name, []))
            wrapper.append(formfields.FormFieldCollection(collection))
        setattr(form, rel_opts.module_name, wrapper)

    c = Context(request, {
        'title': 'Ajout d\'un objet "%s"' % opts.verbose_name,
        "form": form,
        "is_popup": request.REQUEST.has_key("_popup"),
    })
    if object_id_override is not None:
        c['object_id'] = object_id_override
    raw_template = _get_template(opts, app_label, add=True, show_delete=show_delete, form_url=form_url)
#     return HttpResponse(raw_template, mimetype='text/plain')
    t = template_loader.get_template_from_string(raw_template)
    return HttpResponse(t.render(c))

def change_stage(request, app_label, module_name, object_id):
    mod, opts = _get_mod_opts(app_label, module_name)
    if not request.user.has_perm(app_label + '.' + opts.get_change_permission()):
        raise PermissionDenied
    if request.POST and request.POST.has_key("_saveasnew"):
        return add_stage(request, app_label, module_name, form_url='../add/')
    try:
        manipulator = mod.ChangeManipulator(object_id)
    except ObjectDoesNotExist:
        raise Http404
    inline_related_objects = opts.get_inline_related_objects()
    if request.POST:
        new_data = request.POST.copy()
        if opts.has_field_type(meta.FileField):
            new_data.update(request.FILES)
        errors = manipulator.get_validation_errors(new_data)
        if not errors and not request.POST.has_key("_preview"):
            manipulator.do_html2python(new_data)
            new_object = manipulator.save(new_data)

            # On construit le message à enregistrer une fois le changement effectué.
            change_message = []
            if manipulator.fields_added:
                change_message.append('De nouvelles valeurs ont été affectées aux champs %s.' % get_text_list(manipulator.fields_added, 'et'))
            if manipulator.fields_changed:
                change_message.append('Les champs %s ont été modifiés.' % get_text_list(manipulator.fields_changed, 'et'))
            if manipulator.fields_deleted:
                change_message.append('Les valeurs des champs %s ont été supprimées.' % get_text_list(manipulator.fields_deleted, 'et'))
            change_message = ' '.join(change_message)
            if not change_message:
                change_message = 'Aucun champ n\'a été modifié.'

            log.log_action(request.user.id, opts.get_content_type_id(), getattr(new_object, opts.pk.name), repr(new_object), log.CHANGE, change_message)
            msg = 'L\'objet "%s / %s" a bien été modifié.' % (opts.verbose_name, new_object)
            if request.POST.has_key("_continue"):
                request.user.add_message("%s Vous pouvez à nouveau le modifier ci-dessous." % msg)
                if request.REQUEST.has_key('_popup'):
                    return HttpResponseRedirect(request.path + "?_popup=1")
                else:
                    return HttpResponseRedirect(request.path)
            elif request.POST.has_key("_saveasnew"):
                request.user.add_message('L\'objet "%s / %s" a bien été ajouté. Vous pouvez le modifier ci-dessous.' % (opts.verbose_name, new_object))
                return HttpResponseRedirect("../%s/" % new_object.id)
            elif request.POST.has_key("_addanother"):
                request.user.add_message("%s Vous pouvez ajouter un autre objet \"%s\" ci-dessous." % (msg, opts.verbose_name))
                return HttpResponseRedirect("../add/")
            else:
                request.user.add_message(msg)
                return HttpResponseRedirect("../")
        if request.POST.has_key("_preview"):
            manipulator.do_html2python(new_data)
    else:
        # On remplit new_data en lui ajoutant les données actuelles "aplaties".
        new_data = {}
        obj = manipulator.original_object
        for f in opts.fields:
            new_data.update(_get_flattened_data(f, getattr(obj, f.name)))
        for f in opts.many_to_many:
            if not f.rel.edit_inline:
                new_data[f.name] = [i.id for i in getattr(obj, 'get_%s_list' % f.rel.name)()]
        for rel_obj, rel_field in inline_related_objects:
            var_name = rel_obj.object_name.lower()
            for i, rel_instance in enumerate(getattr(obj, 'get_%s_list' % opts.get_rel_object_method_name(rel_obj, rel_field))()):
                for f in rel_obj.fields:
                    if f.editable and f != rel_field:
                        for k, v in _get_flattened_data(f, getattr(rel_instance, f.name)).items():
                            new_data['%s.%d.%s' % (var_name, i, k)] = v
                for f in rel_obj.many_to_many:
                    new_data['%s.%d.%s' % (var_name, i, f.name)] = [j.id for j in getattr(rel_instance, 'get_%s_list' % f.rel.name)()]

        # Si l'objet contient sur sa page de l'interface d'administration des objets devant être
        # triés en fonction de lui-même, on va chercher l'ordre de ces objets et on "aplatit" cet
        # ordre de manière à obtenir une liste d'identifiants séparés entre eux par des virgules.
        id_order_list = []
        for rel_obj in opts.get_ordered_objects():
            id_order_list.extend(getattr(obj, 'get_%s_order' % rel_obj.object_name.lower())())
        if id_order_list:
            new_data['order_'] = ','.join(map(str, id_order_list))
        errors = {}

    # On remplit l'objet FormWrapper.
    form = formfields.FormWrapper(manipulator, new_data, errors)
    form.original = manipulator.original_object
    form.order_objects = []
    for rel_opts, rel_field in inline_related_objects:
        var_name = rel_opts.object_name.lower()
        wrapper = []
        orig_list = getattr(manipulator.original_object, 'get_%s_list' % opts.get_rel_object_method_name(rel_opts, rel_field))()
        count = len(orig_list) + rel_field.rel.num_extra_on_change
        if rel_field.rel.min_num_in_admin:
            count = max(count, rel_field.rel.min_num_in_admin)
        if rel_field.rel.max_num_in_admin:
            count = min(count, rel_field.rel.max_num_in_admin)
        for i in range(count):
            collection = {'original': (i < len(orig_list) and orig_list[i] or None)}
            for f in rel_opts.fields + rel_opts.many_to_many:
                if f.editable and f != rel_field:
                    for field_name in f.get_manipulator_field_names(''):
                        full_field_name = '%s.%d.%s' % (var_name, i, field_name)
                        collection[field_name] = formfields.FormFieldWrapper(manipulator[full_field_name], new_data.get(full_field_name, ''), errors.get(full_field_name, []))
            wrapper.append(formfields.FormFieldCollection(collection))
        setattr(form, rel_opts.module_name, wrapper)
        if rel_opts.order_with_respect_to and rel_opts.order_with_respect_to.rel and rel_opts.order_with_respect_to.rel.to == opts:
            form.order_objects.extend(orig_list)

    c = Context(request, {
        'title': 'Modification d\'un objet "%s"' % opts.verbose_name,
        "form": form,
        'object_id': object_id,
        'original': manipulator.original_object,
        'is_popup' : request.REQUEST.has_key('_popup'),
    })
    raw_template = _get_template(opts, app_label, change=True)
#     return HttpResponse(raw_template, mimetype='text/plain')
    t = template_loader.get_template_from_string(raw_template)
    return HttpResponse(t.render(c))

def _nest_help(obj, depth, val):
    current = obj
    for i in range(depth):
        current = current[-1]
    current.append(val)

def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth):
    "Fonction assistante qui se charge de remplir récursivement la variable deleted_objects."
    nh = _nest_help # Pour des raisons de performance, on affecte la fonction _nest_help à une variable locale
    if current_depth > 16:
        return # On arrête la fonction si la récursion va trop loin.
    objects_seen = []
    for rel_opts, rel_field in opts.get_all_related_objects():
        if rel_opts in objects_seen:
            continue
        objects_seen.append(rel_opts)
        rel_opts_name = opts.get_rel_object_method_name(rel_opts, rel_field)
        if isinstance(rel_field.rel, meta.OneToOne):
            try:
                sub_obj = getattr(obj, 'get_%s' % rel_opts_name)()
            except ObjectDoesNotExist:
                pass
            else:
                if rel_opts.admin:
                    p = '%s.%s' % (rel_opts.app_label, rel_opts.get_delete_permission())
                    if not user.has_perm(p):
                        perms_needed.add(rel_opts.verbose_name)
                        # Ici, on se fiche de remplir deleted_objects.
                        continue
                if rel_field.rel.edit_inline or not rel_opts.admin:
                    # On n'affiche pas de lien permettant de modifier l'objet, car
                    # il ne possède pas d'interface d'administration dédiée ou il est
                    # modifié directement sur la page d'un autre objet.
                    nh(deleted_objects, current_depth, ['%s : %r' % (meta.capfirst(rel_opts.verbose_name), sub_obj), []])
                else:
                    # On affiche un lien menant à la page d'administration de l'objet.
                    nh(deleted_objects, current_depth, ['%s : <a href="../../../../%s/%s/%s/">%r</a>' % \
                        (meta.capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name,
                        getattr(sub_obj, rel_opts.pk.name), sub_obj), []])
                _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_opts, current_depth+2)
        else:
            has_related_objs = False
            for sub_obj in getattr(obj, 'get_%s_list' % rel_opts_name)():
                has_related_objs = True
                if rel_field.rel.edit_inline or not rel_opts.admin:
                    # On n'affiche pas de lien permettant de modifier l'objet, car
                    # il ne possède pas d'interface d'administration dédiée ou il est
                    # modifié directement sur la page d'un autre objet.
                    nh(deleted_objects, current_depth, ['%s : %s' % (meta.capfirst(rel_opts.verbose_name), strip_tags(repr(sub_obj))), []])
                else:
                    # On affiche un lien menant à la page d'administration de l'objet.
                    nh(deleted_objects, current_depth, ['%s : <a href="../../../../%s/%s/%s/">%s</a>' % \
                        (meta.capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name, sub_obj.id, strip_tags(repr(sub_obj))), []])
                _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_opts, current_depth+2)
            # Si des objets connexes existent et si l'utilisateur actuel n'a pas
            # la permission de les supprimer, on ajoute la permission manquante à
            # la variable perms_needed.
            if rel_opts.admin and has_related_objs:
                p = '%s.%s' % (rel_opts.app_label, rel_opts.get_delete_permission())
                if not user.has_perm(p):
                    perms_needed.add(rel_opts.verbose_name)
    for rel_opts, rel_field in opts.get_all_related_many_to_many_objects():
        if rel_opts in objects_seen:
            continue
        objects_seen.append(rel_opts)
        rel_opts_name = opts.get_rel_object_method_name(rel_opts, rel_field)
        has_related_objs = False
        for sub_obj in getattr(obj, 'get_%s_list' % rel_opts_name)():
            has_related_objs = True
            if rel_field.rel.edit_inline or not rel_opts.admin:
                # On n'affiche pas de lien permettant de modifier l'objet, car
                # il ne possède pas d'interface d'administration dédiée ou il est
                # modifié directement sur la page d'un autre objet.
                nh(deleted_objects, current_depth, ['Un ou plusieurs objets "%s" correspondant au champ %s : %s' % \
                    (rel_opts.verbose_name, rel_field.name, strip_tags(repr(sub_obj))), []])
            else:
                # On affiche un lien menant à la page d'administration de l'objet.
                nh(deleted_objects, current_depth, ['Un ou plusieurs objets "%s" correspondant au champ %s : <a href="../../../../%s/%s/%s/">%s</a>' % \
                    (rel_opts.verbose_name, rel_field.name, rel_opts.app_label, rel_opts.module_name, sub_obj.id, strip_tags(repr(sub_obj))), []])
        # Si des objets connexes existent et si l'utilisateur actuel n'a pas
        # la permission de les modifier, on ajoute la permission manquante à
        # la variable perms_needed.
        if rel_opts.admin and has_related_objs:
            p = '%s.%s' % (rel_opts.app_label, rel_opts.get_change_permission())
            if not user.has_perm(p):
                perms_needed.add(rel_opts.verbose_name)

def delete_stage(request, app_label, module_name, object_id):
    import sets
    mod, opts = _get_mod_opts(app_label, module_name)
    if not request.user.has_perm(app_label + '.' + opts.get_delete_permission()):
        raise PermissionDenied
    try:
        obj = mod.get_object(**{'%s__exact' % opts.pk.name: object_id})
    except ObjectDoesNotExist:
        raise Http404

    # On remplit la variable deleted_objects, une structure de données contenant
    # tous les objets connexes qui seront également supprimés.
    deleted_objects = ['Objet "%s" : <a href="../../%s/">%s</a>' % (opts.verbose_name, object_id, strip_tags(repr(obj))), []]
    perms_needed = sets.Set()
    _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1)

    if request.POST: # L'utilisateur a déjà confirmé la suppression.
        if perms_needed:
            raise PermissionDenied
        obj.delete()
        obj_repr = repr(obj)
        log.log_action(request.user.id, opts.get_content_type_id(), object_id, obj_repr, log.DELETION)
        request.user.add_message('L\'objet "%s / %s" a bien été supprimé.' % (opts.verbose_name, obj_repr))
        return HttpResponseRedirect("../../")
    t = template_loader.get_template("delete_confirmation_generic")
    c = Context(request, {
        "title": "Êtes-vous certain?",
        "object_name": opts.verbose_name,
        "object": obj,
        "deleted_objects": deleted_objects,
        "perms_lacking": perms_needed,
    })
    return HttpResponse(t.render(c))

def history(request, app_label, module_name, object_id):
    mod, opts = _get_mod_opts(app_label, module_name)
    action_list = log.get_list(object_id__exact=object_id, content_type_id__exact=opts.get_content_type_id(),
        order_by=(("action_time", "ASC"),), select_related=True)
    # Si aucun historique n'a été trouvé, on vérifie que l'objet existe (ce serait
    # mieux quand même).
    try:
        obj = mod.get_object(id__exact=object_id)
    except ObjectDoesNotExist:
        raise Http404
    t = template_loader.get_template('admin_object_history')
    c = Context(request, {
        'title': 'Historique des modifications apportées à l\'objet "%r"' % obj,
        'action_list': action_list,
        'module_name': meta.capfirst(opts.verbose_name_plural),
        'object': obj,
    })
    return HttpResponse(t.render(c))
